3.2 KernelLoader
KernelLoader 是连接 Dart 源代码(经过前端编译器处理后的内核格式)和 DartVM 运行时的桥梁。它负责将静态的程序表示转换为可以被 VM 执行的动态结构。这个类的设计和实现直接影响了 DartVM 的性能、内存使用和功能支持(如热重载、调试等)。
KernelLoader 是 DartVM 中负责加载和处理 Dart 内核(Kernel)格式代码的关键类。它的主要作用包括:
-
加载程序:通过
LoadProgram
方法加载整个 Dart 程序。这个方法会解析内核格式的代码,创建相应的 VM 内部结构(如类、函数等)。 -
加载库:
LoadLibrary
方法用于加载单个库。这对于增量编译和热重载等场景非常重要。 -
加载表达式:
LoadExpressionEvaluationFunction
方法用于加载和编译表达式评估函数,这在调试器中进行表达式求值时很有用。 -
查找修改的库:
FindModifiedLibraries
方法用于在增量编译时识别哪些库被修改了,这对于有效地更新代码很重要。 -
处理类和函数: 包含了处理类(
LoadClass
)和函数(LoadProcedure
)的方法,这些方法负责将内核格式的类和函数定义转换为 VM 内部的表示。 -
管理元数据:处理注释(
ReadVMAnnotations
)和其他元数据,这些信息对于反射和工具支持很重要。 -
处理常量:包含了读取和处理编译时常量的逻辑,这对于优化和静态分析很重要。
-
类型处理:包含了处理类型参数、协变性等与 Dart 类型系统相关的逻辑。
-
内存管理:管理加载过程中的内存分配,确保高效和正确的内存使用。
-
错误处理:提供了错误报告和处理机制,确保在加载过程中遇到问题时能够优雅地处理。
LoadEntireProgram
kernel::KernelLoader::LoadEntireProgram
是将 Kernel AST 反序列化为相应 VM 对象的入口点。方法签名,接受一个 Program 指针和一个布尔值参数:
Object& KernelLoader::LoadEntireProgram(Program* program,
bool process_pending_classes) {
前置准备:
// 获取当前线程的指针。
Thread* thread = Thread::Current();
// 这是一个性能计时宏,用于记录加载内核的时间。
TIMELINE_DURATION(thread, Isolate, "LoadKernel");
如果是单个程序,则创建一个 KernelLoader 实例并直接加载程序。
if (program->is_single_program()) {
KernelLoader loader(program, /*uri_to_source_table=*/nullptr);
return Object::Handle(loader.LoadProgram(process_pending_classes));
}
如果不是单个程序,创建一个数组来存储子程序的起始位置,然后使用 index_programs 函数来填充这个数组。
GrowableArray<intptr_t> subprogram_file_starts;
{
kernel::Reader reader(program->binary());
index_programs(&reader, &subprogram_file_starts);
}
获取当前线程的内存区域,创建一个 Library 句柄,计算子程序的数量。
Zone* zone = thread->zone();
Library& library = Library::Handle(zone);
intptr_t subprogram_count = subprogram_file_starts.length() - 1;
接下来的代码主要是处理多个子程序的情况,包括:
- 索引所有源代码表
- 为每个子程序创建"fake programs"并加载它们
- 如果需要,处理待处理的类
这段代码的主要目的是为所有子程序构建一个统一的源代码表,同时确保不同子程序之间没有冲突的源代码定义。
如果找到了匹配的条目,检查是否存在冲突;如果没有找到,则创建新的条目并插入到映射表中。
// First index all source tables.
// 创建一个 UriToSourceTable 对象,用于存储 URI 到源代码的映射。
UriToSourceTable uri_to_source_table;
// 创建一个 UriToSourceTableEntry 对象,用作临时包装器。
UriToSourceTableEntry wrapper;
// 获取当前线程和其关联的内存区域(Zone)。
Thread* thread_ = Thread::Current();
Zone* zone_ = thread_->zone();
// 开始一个循环,从最后一个子程序开始向前遍历所有子程序。
for (intptr_t i = subprogram_count - 1; i >= 0; --i) {
// 获取当前子程序的起始和结束位置。
intptr_t subprogram_start = subprogram_file_starts.At(i);
intptr_t subprogram_end = subprogram_file_starts.At(i + 1);
// 创建一个 TypedDataBase 句柄,表示当前子程序的二进制数据。
const auto& component = TypedDataBase::Handle(
program->binary().ViewFromTo(subprogram_start, subprogram_end));
// 创建 TranslationHelper 和 KernelReaderHelper 对象,用于辅助读取内核数据。
TranslationHelper translation_helper(thread);
KernelReaderHelper helper_(zone_, &translation_helper, component, 0);
// 获取源代码表的大小。
const intptr_t source_table_size = helper_.SourceTableSize();
// 开始遍历源代码表中的每一项。
for (intptr_t index = 0; index < source_table_size; ++index) {
// 获取当前项的 URI 字符串,并将其设置为包装器的 URI。
const String& uri_string = helper_.SourceTableUriFor(index);
wrapper.uri = &uri_string;
// 获取行起始位置数据,如果为空则跳过当前项。
TypedData& line_starts =
TypedData::Handle(Z, helper_.GetLineStartsFor(index));
if (line_starts.Length() == 0) continue;
// 获取脚本源代码,并再次设置包装器的 URI(这里可能是冗余的)。
const String& script_source = helper_.GetSourceFor(index);
wrapper.uri = &uri_string;
// 在 URI 到源代码的映射表中查找当前 URI。
UriToSourceTableEntry* pair = uri_to_source_table.LookupValue(&wrapper);
if (pair != nullptr) {
// // 检查是否存在冲突的源代码条目
// At least two entries with content. Unless the content is the same
// that's not valid.
const bool src_differ = pair->sources->CompareTo(script_source) != 0;
const bool line_starts_differ =
!pair->line_starts->CanonicalizeEquals(line_starts);
if (src_differ || line_starts_differ) {
FATAL(/* 错误信息 */);
}
} else {
// // 创建新的条目并插入到映射表中
UriToSourceTableEntry* tmp = new UriToSourceTableEntry();
tmp->uri = &uri_string;
tmp->sources = &script_source;
tmp->line_starts = &line_starts;
uri_to_source_table.Insert(tmp);
}
}
}
这段代码的主要目的是为每个子程序创建一个"假程序",加载它,并处理加载结果。如果加载成功并且结果是一个库,它会更新全局的 library 变量。这个过程允许 DartVM 处理可能由多个部分组成的复杂程序。
// Create "fake programs" for each sub-program.
// 这个循环为每个子程序创建"假程序"。它从最后一个子程序开始,向前遍历。
for (intptr_t i = subprogram_count - 1; i >= 0; --i) {
// 获取当前子程序的起始和结束位置。
intptr_t subprogram_start = subprogram_file_starts.At(i);
intptr_t subprogram_end = subprogram_file_starts.At(i + 1);
// 创建一个 TypedDataBase 句柄,表示当前子程序的二进制数据。
const auto& component = TypedDataBase::Handle(
program->binary().ViewFromTo(subprogram_start, subprogram_end));
// 创建一个 Reader 对象来读取这个组件。
Reader reader(component);
const char* error = nullptr;
// 尝试从reader中读取一个Program对象。使用unique_ptr来管理内存。
std::unique_ptr<Program> subprogram = Program::ReadFrom(&reader, &error);
// 如果读取失败,输出错误信息并终止程序。
if (subprogram == nullptr) {
FATAL("Failed to load kernel file: %s", error);
}
// 断言确保读取的是单个程序。
ASSERT(subprogram->is_single_program());
// 创建一个KernelLoader对象来加载这个子程序。
KernelLoader loader(subprogram.get(), &uri_to_source_table);
// 加载程序并获取结果。
Object& load_result = Object::Handle(loader.LoadProgram(false));
// 如果加载结果是错误,直接返回这个错误。
if (load_result.IsError()) return load_result;
// 如果加载结果是一个库,更新 library 变量。
// 这里使用了位异或操作符^=,这在Dart VM中通常用于更新对象引用。
if (load_result.IsLibrary()) {
library ^= load_result.ptr();
}
}
最后,根据处理结果返回适当的对象(库或错误)。
if (process_pending_classes && !ClassFinalizer::ProcessPendingClasses()) {
// Class finalization failed -> sticky error would be set.
return Error::Handle(thread->StealStickyError());
}
return library;
LoadProgram
这个方法负责加载整个 Dart 程序,包括所有的库、类和常量。它处理了错误情况,确保了线程安全,并设置了程序的初始状态。这是 Dart VM 启动和运行 Dart 程序的关键部分。
这是方法的定义。它接受一个布尔参数 process_pending_classes
,返回一个 ObjectPtr
。这个方法的目的是加载整个 Dart 程序。
ObjectPtr KernelLoader::LoadProgram(bool process_pending_classes) {
前置检查:
// 这行创建了一个安全点写锁。
// 它确保在加载程序时,其他线程不会修改程序的状态。这是为了线程安全。
SafepointWriteRwLocker ml(thread_, thread_->isolate_group()->program_lock());
// 这个断言检查 `kernel_program_info_` 的常量数组是否为空。
// 这确保我们还没有加载任何常量。
ASSERT(kernel_program_info_.constants() == Array::null());
// 这个检查确保我们不是在尝试加载一个连接的 dill 文件
// (多个 Dart 程序合并成一个文件)。如果是,程序会终止并显示错误消息。
if (!program_->is_single_program())
{
FATAL(
"Trying to load a concatenated dill file at a time where that is "
"not allowed");
}
核心逻辑:
// 这设置了一个长跳转(longjump)作用域。
// 这是用于错误处理的 - 如果在加载过程中发生严重错误,我们可以跳转到这里。
LongJumpScope jump;
if (setjmp(*jump.Set()) == 0)
{
// Note that `problemsAsJson` on Component is implicitly skipped.
// 这个循环遍历程序中的所有库,并调用 `LoadLibrary` 方法来加载每个库。
const intptr_t length = program_->library_count();
for (intptr_t i = 0; i < length; i++)
{
LoadLibrary(i);
}
// 如果 `process_pending_classes` 为真,这段代码会处理所有待处理的类。
// 如果处理失败,它会返回一个错误。
// Finalize still pending classes if requested.
if (process_pending_classes)
{
if ProcessPendingClasses()
{
// Class finalization failed -> sticky error would be set.
return H.thread()->StealStickyError();
}
}
// Sets the constants array to an empty array with the length equal to
// the number of constants. The array gets filled lazily while reading
// constants.
// 这段代码为常量创建一个数组。
// 它首先检查常量表是否存在,然后创建一个新的数组来存储所有常量。
// 初始时,所有元素都被设置为哨兵对象。
ASSERT(kernel_program_info_.constants_table() != ExternalTypedData::null());
ConstantReader constant_reader(&helper_, &active_class_);
// 所有常量的数量
const intptr_t num_consts = constant_reader.NumConstants();
// 存储常量的数组
const Array &array = Array::Handle(Z, Array::New(num_consts, Heap::kOld));
for (intptr_t i = 0; i < num_consts; i++)
{
array.SetAt(i, Object::sentinel());
}
kernel_program_info_.set_constants(array);
H.SetConstants(array); // for caching
// 这段代码查找程序的主方法。
// 如果找到了主方法,它会返回包含主方法的库。如果没有主方法,它返回 null。
NameIndex main = program_->main_method();
if (main != -1)
{
NameIndex main_library = H.EnclosingName(main);
return LookupLibrary(main_library);
}
return Library::null();
}
// Either class finalization failed or we caught a compile error.
// In both cases sticky error would be set.
// 如果发生了长跳转(即出现了错误),这行代码会返回当前线程的"粘性"错误。
return Thread::Current()->StealStickyError();
本文作者:Maeiee
本文链接:3.2 KernelLoader
版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!
喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!